{
  "name": "action-bar",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "button",
    "@diceui/use-as-ref",
    "@diceui/use-isomorphic-layout-effect"
  ],
  "files": [
    {
      "path": "ui/action-bar.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  Direction as DirectionPrimitive,\n  Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { Button } from \"@/registry/bases/radix/ui/button\";\n\nconst ROOT_NAME = \"ActionBar\";\nconst GROUP_NAME = \"ActionBarGroup\";\nconst ITEM_NAME = \"ActionBarItem\";\nconst CLOSE_NAME = \"ActionBarClose\";\nconst SEPARATOR_NAME = \"ActionBarSeparator\";\nconst ITEM_SELECT = \"actionbar.itemSelect\";\nconst ENTRY_FOCUS = \"actionbarFocusGroup.onEntryFocus\";\nconst EVENT_OPTIONS = { bubbles: false, cancelable: true };\n\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"horizontal\" | \"vertical\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ntype RootElement = React.ComponentRef<typeof ActionBar>;\ntype ItemElement = React.ComponentRef<typeof ActionBarItem>;\ntype CloseElement = React.ComponentRef<typeof ActionBarClose>;\n\nfunction focusFirst(\n  candidates: React.RefObject<HTMLElement | null>[],\n  preventScroll = false,\n) {\n  const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;\n  for (const candidateRef of candidates) {\n    const candidate = candidateRef.current;\n    if (!candidate) continue;\n    if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;\n    candidate.focus({ preventScroll });\n    if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;\n  }\n}\n\nfunction wrapArray<T>(array: T[], startIndex: number) {\n  return array.map<T>(\n    (_, index) => array[(startIndex + index) % array.length] as T,\n  );\n}\n\nfunction getDirectionAwareKey(key: string, dir?: Direction) {\n  if (dir !== \"rtl\") return key;\n  return key === \"ArrowLeft\"\n    ? \"ArrowRight\"\n    : key === \"ArrowRight\"\n      ? \"ArrowLeft\"\n      : key;\n}\n\ninterface ItemData {\n  id: string;\n  ref: React.RefObject<ItemElement | null>;\n  disabled: boolean;\n}\n\ninterface ActionBarContextValue {\n  onOpenChange?: (open: boolean) => void;\n  dir: Direction;\n  orientation: Orientation;\n  loop: boolean;\n}\n\nconst ActionBarContext = React.createContext<ActionBarContextValue | null>(\n  null,\n);\n\nfunction useActionBarContext(consumerName: string) {\n  const context = React.useContext(ActionBarContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface FocusContextValue {\n  tabStopId: string | null;\n  onItemFocus: (tabStopId: string) => void;\n  onItemShiftTab: () => void;\n  onFocusableItemAdd: () => void;\n  onFocusableItemRemove: () => void;\n  onItemRegister: (item: ItemData) => void;\n  onItemUnregister: (id: string) => void;\n  getItems: () => ItemData[];\n}\n\nconst FocusContext = React.createContext<FocusContextValue | null>(null);\n\nfunction useFocusContext(consumerName: string) {\n  const context = React.useContext(FocusContext);\n  if (!context) {\n    throw new Error(\n      `\\`${consumerName}\\` must be used within \\`FocusProvider\\``,\n    );\n  }\n  return context;\n}\n\ninterface ActionBarProps extends DivProps {\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onEscapeKeyDown?: (event: KeyboardEvent) => void;\n  align?: \"start\" | \"center\" | \"end\";\n  alignOffset?: number;\n  side?: \"top\" | \"bottom\";\n  sideOffset?: number;\n  portalContainer?: Element | DocumentFragment | null;\n  dir?: Direction;\n  orientation?: Orientation;\n  loop?: boolean;\n}\n\nfunction ActionBar(props: ActionBarProps) {\n  const {\n    open = false,\n    onOpenChange,\n    onEscapeKeyDown,\n    side = \"bottom\",\n    alignOffset = 0,\n    align = \"center\",\n    sideOffset = 16,\n    portalContainer: portalContainerProp,\n    dir: dirProp,\n    orientation = \"horizontal\",\n    loop = true,\n    className,\n    style,\n    ref,\n    asChild,\n    ...rootProps\n  } = props;\n\n  const [mounted, setMounted] = React.useState(false);\n\n  const rootRef = React.useRef<RootElement>(null);\n  const composedRef = useComposedRefs(ref, rootRef);\n\n  const propsRef = useAsRef({\n    onEscapeKeyDown,\n    onOpenChange,\n  });\n\n  const dir = DirectionPrimitive.useDirection(dirProp);\n\n  React.useLayoutEffect(() => {\n    setMounted(true);\n  }, []);\n\n  React.useEffect(() => {\n    if (!open) return;\n\n    const ownerDocument = rootRef.current?.ownerDocument ?? document;\n\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key === \"Escape\") {\n        propsRef.current.onEscapeKeyDown?.(event);\n        if (!event.defaultPrevented) {\n          propsRef.current.onOpenChange?.(false);\n        }\n      }\n    }\n\n    ownerDocument.addEventListener(\"keydown\", onKeyDown);\n    return () => ownerDocument.removeEventListener(\"keydown\", onKeyDown);\n  }, [open, propsRef]);\n\n  const contextValue = React.useMemo<ActionBarContextValue>(\n    () => ({\n      onOpenChange,\n      dir,\n      orientation,\n      loop,\n    }),\n    [onOpenChange, dir, orientation, loop],\n  );\n\n  const portalContainer =\n    portalContainerProp ?? (mounted ? globalThis.document?.body : null);\n\n  if (!portalContainer || !open) return null;\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ActionBarContext.Provider value={contextValue}>\n      {ReactDOM.createPortal(\n        <RootPrimitive\n          role=\"toolbar\"\n          aria-orientation={orientation}\n          data-slot=\"action-bar\"\n          data-side={side}\n          data-align={align}\n          data-orientation={orientation}\n          dir={dir}\n          {...rootProps}\n          ref={composedRef}\n          className={cn(\n            \"fixed z-50 rounded-lg border bg-card shadow-lg outline-none\",\n            \"fade-in-0 zoom-in-95 animate-in duration-250 [animation-timing-function:cubic-bezier(0.16,1,0.3,1)]\",\n            \"data-[side=bottom]:slide-in-from-bottom-4 data-[side=top]:slide-in-from-top-4\",\n            \"motion-reduce:animate-none motion-reduce:transition-none\",\n            orientation === \"horizontal\"\n              ? \"flex flex-row items-center gap-2 px-2 py-1.5\"\n              : \"flex flex-col items-start gap-2 px-1.5 py-2\",\n            className,\n          )}\n          style={{\n            [side]: `${sideOffset}px`,\n            ...(align === \"center\" && {\n              left: \"50%\",\n              translate: \"-50% 0\",\n            }),\n            ...(align === \"start\" && { left: `${alignOffset}px` }),\n            ...(align === \"end\" && { right: `${alignOffset}px` }),\n            ...style,\n          }}\n        />,\n        portalContainer,\n      )}\n    </ActionBarContext.Provider>\n  );\n}\n\nfunction ActionBarSelection(props: DivProps) {\n  const { className, asChild, ...selectionProps } = props;\n\n  const SelectionPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SelectionPrimitive\n      data-slot=\"action-bar-selection\"\n      {...selectionProps}\n      className={cn(\n        \"flex items-center gap-1 rounded-sm border px-2 py-1 font-medium text-sm tabular-nums\",\n        className,\n      )}\n    />\n  );\n}\n\nfunction ActionBarGroup(props: DivProps) {\n  const {\n    onBlur: onBlurProp,\n    onFocus: onFocusProp,\n    onMouseDown: onMouseDownProp,\n    className,\n    asChild,\n    ref,\n    ...groupProps\n  } = props;\n\n  const [tabStopId, setTabStopId] = React.useState<string | null>(null);\n  const [isTabbingBackOut, setIsTabbingBackOut] = React.useState(false);\n  const [focusableItemCount, setFocusableItemCount] = React.useState(0);\n\n  const groupRef = React.useRef<HTMLDivElement>(null);\n  const composedRef = useComposedRefs(ref, groupRef);\n  const isClickFocusRef = React.useRef(false);\n  const itemsRef = React.useRef<Map<string, ItemData>>(new Map());\n\n  const { dir, orientation } = useActionBarContext(GROUP_NAME);\n\n  const onItemFocus = React.useCallback((tabStopId: string) => {\n    setTabStopId(tabStopId);\n  }, []);\n\n  const onItemShiftTab = React.useCallback(() => {\n    setIsTabbingBackOut(true);\n  }, []);\n\n  const onFocusableItemAdd = React.useCallback(() => {\n    setFocusableItemCount((prevCount) => prevCount + 1);\n  }, []);\n\n  const onFocusableItemRemove = React.useCallback(() => {\n    setFocusableItemCount((prevCount) => prevCount - 1);\n  }, []);\n\n  const onItemRegister = React.useCallback((item: ItemData) => {\n    itemsRef.current.set(item.id, item);\n  }, []);\n\n  const onItemUnregister = React.useCallback((id: string) => {\n    itemsRef.current.delete(id);\n  }, []);\n\n  const getItems = React.useCallback(() => {\n    return Array.from(itemsRef.current.values())\n      .filter((item) => item.ref.current)\n      .sort((a, b) => {\n        const elementA = a.ref.current;\n        const elementB = b.ref.current;\n        if (!elementA || !elementB) return 0;\n        const position = elementA.compareDocumentPosition(elementB);\n        if (position & Node.DOCUMENT_POSITION_FOLLOWING) {\n          return -1;\n        }\n        if (position & Node.DOCUMENT_POSITION_PRECEDING) {\n          return 1;\n        }\n        return 0;\n      });\n  }, []);\n\n  const onBlur = React.useCallback(\n    (event: React.FocusEvent<HTMLDivElement>) => {\n      onBlurProp?.(event);\n      if (event.defaultPrevented) return;\n\n      setIsTabbingBackOut(false);\n    },\n    [onBlurProp],\n  );\n\n  const onFocus = React.useCallback(\n    (event: React.FocusEvent<HTMLDivElement>) => {\n      onFocusProp?.(event);\n      if (event.defaultPrevented) return;\n\n      const isKeyboardFocus = !isClickFocusRef.current;\n      if (\n        event.target === event.currentTarget &&\n        isKeyboardFocus &&\n        !isTabbingBackOut\n      ) {\n        const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS);\n        event.currentTarget.dispatchEvent(entryFocusEvent);\n\n        if (!entryFocusEvent.defaultPrevented) {\n          const items = Array.from(itemsRef.current.values()).filter(\n            (item) => !item.disabled,\n          );\n          const currentItem = items.find((item) => item.id === tabStopId);\n\n          const candidateItems = [currentItem, ...items].filter(\n            Boolean,\n          ) as ItemData[];\n          const candidateRefs = candidateItems.map((item) => item.ref);\n          focusFirst(candidateRefs, false);\n        }\n      }\n      isClickFocusRef.current = false;\n    },\n    [onFocusProp, isTabbingBackOut, tabStopId],\n  );\n\n  const onMouseDown = React.useCallback(\n    (event: React.MouseEvent<HTMLDivElement>) => {\n      onMouseDownProp?.(event);\n      if (event.defaultPrevented) return;\n\n      isClickFocusRef.current = true;\n    },\n    [onMouseDownProp],\n  );\n\n  const focusContextValue = React.useMemo<FocusContextValue>(\n    () => ({\n      tabStopId,\n      onItemFocus,\n      onItemShiftTab,\n      onFocusableItemAdd,\n      onFocusableItemRemove,\n      onItemRegister,\n      onItemUnregister,\n      getItems,\n    }),\n    [\n      tabStopId,\n      onItemFocus,\n      onItemShiftTab,\n      onFocusableItemAdd,\n      onFocusableItemRemove,\n      onItemRegister,\n      onItemUnregister,\n      getItems,\n    ],\n  );\n\n  const GroupPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <FocusContext.Provider value={focusContextValue}>\n      <GroupPrimitive\n        role=\"group\"\n        data-slot=\"action-bar-group\"\n        data-orientation={orientation}\n        dir={dir}\n        tabIndex={isTabbingBackOut || focusableItemCount === 0 ? -1 : 0}\n        {...groupProps}\n        ref={composedRef}\n        className={cn(\n          \"flex gap-2 outline-none\",\n          orientation === \"horizontal\"\n            ? \"items-center\"\n            : \"w-full flex-col items-start\",\n          className,\n        )}\n        onBlur={onBlur}\n        onFocus={onFocus}\n        onMouseDown={onMouseDown}\n      />\n    </FocusContext.Provider>\n  );\n}\n\ninterface ActionBarItemProps\n  extends Omit<React.ComponentProps<typeof Button>, \"onSelect\"> {\n  onSelect?: (event: Event) => void;\n}\n\nfunction ActionBarItem(props: ActionBarItemProps) {\n  const {\n    onSelect,\n    onClick: onClickProp,\n    onFocus: onFocusProp,\n    onKeyDown: onKeyDownProp,\n    onMouseDown: onMouseDownProp,\n    className,\n    disabled,\n    ref,\n    ...itemProps\n  } = props;\n\n  const itemRef = React.useRef<ItemElement>(null);\n  const composedRef = useComposedRefs(ref, itemRef);\n  const isMouseClickRef = React.useRef(false);\n\n  const { onOpenChange, dir, orientation, loop } =\n    useActionBarContext(ITEM_NAME);\n  const focusContext = useFocusContext(ITEM_NAME);\n\n  const itemId = React.useId();\n  const isTabStop = focusContext.tabStopId === itemId;\n\n  useIsomorphicLayoutEffect(() => {\n    focusContext.onItemRegister({\n      id: itemId,\n      ref: itemRef,\n      disabled: !!disabled,\n    });\n\n    if (!disabled) {\n      focusContext.onFocusableItemAdd();\n    }\n\n    return () => {\n      focusContext.onItemUnregister(itemId);\n      if (!disabled) {\n        focusContext.onFocusableItemRemove();\n      }\n    };\n  }, [focusContext, itemId, disabled]);\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<ItemElement>) => {\n      onClickProp?.(event);\n      if (event.defaultPrevented) return;\n\n      const item = itemRef.current;\n      if (!item) return;\n\n      const itemSelectEvent = new CustomEvent(ITEM_SELECT, {\n        bubbles: true,\n        cancelable: true,\n      });\n\n      item.addEventListener(ITEM_SELECT, (event) => onSelect?.(event), {\n        once: true,\n      });\n\n      item.dispatchEvent(itemSelectEvent);\n\n      if (!itemSelectEvent.defaultPrevented) {\n        onOpenChange?.(false);\n      }\n    },\n    [onClickProp, onOpenChange, onSelect],\n  );\n\n  const onFocus = React.useCallback(\n    (event: React.FocusEvent<ItemElement>) => {\n      onFocusProp?.(event);\n      if (event.defaultPrevented) return;\n\n      focusContext.onItemFocus(itemId);\n      isMouseClickRef.current = false;\n    },\n    [onFocusProp, focusContext, itemId],\n  );\n\n  const onKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<ItemElement>) => {\n      onKeyDownProp?.(event);\n      if (event.defaultPrevented) return;\n\n      if (event.key === \"Tab\" && event.shiftKey) {\n        focusContext.onItemShiftTab();\n        return;\n      }\n\n      if (event.target !== event.currentTarget) return;\n\n      const key = getDirectionAwareKey(event.key, dir);\n      let focusIntent: \"first\" | \"last\" | \"prev\" | \"next\" | undefined;\n\n      if (orientation === \"horizontal\") {\n        if (key === \"ArrowLeft\") focusIntent = \"prev\";\n        else if (key === \"ArrowRight\") focusIntent = \"next\";\n        else if (key === \"Home\") focusIntent = \"first\";\n        else if (key === \"End\") focusIntent = \"last\";\n      } else {\n        if (key === \"ArrowUp\") focusIntent = \"prev\";\n        else if (key === \"ArrowDown\") focusIntent = \"next\";\n        else if (key === \"Home\") focusIntent = \"first\";\n        else if (key === \"End\") focusIntent = \"last\";\n      }\n\n      if (focusIntent !== undefined) {\n        if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)\n          return;\n        event.preventDefault();\n\n        const items = focusContext.getItems().filter((item) => !item.disabled);\n        let candidateRefs = items.map((item) => item.ref);\n\n        if (focusIntent === \"last\") {\n          candidateRefs.reverse();\n        } else if (focusIntent === \"prev\" || focusIntent === \"next\") {\n          if (focusIntent === \"prev\") candidateRefs.reverse();\n          const currentIndex = candidateRefs.findIndex(\n            (ref) => ref.current === event.currentTarget,\n          );\n          candidateRefs = loop\n            ? wrapArray(candidateRefs, currentIndex + 1)\n            : candidateRefs.slice(currentIndex + 1);\n        }\n\n        queueMicrotask(() => focusFirst(candidateRefs));\n      }\n    },\n    [onKeyDownProp, focusContext, dir, orientation, loop],\n  );\n\n  const onMouseDown = React.useCallback(\n    (event: React.MouseEvent<ItemElement>) => {\n      onMouseDownProp?.(event);\n      if (event.defaultPrevented) return;\n\n      isMouseClickRef.current = true;\n\n      if (disabled) {\n        event.preventDefault();\n      } else {\n        focusContext.onItemFocus(itemId);\n      }\n    },\n    [onMouseDownProp, focusContext, itemId, disabled],\n  );\n\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"action-bar-item\"\n      variant=\"secondary\"\n      size=\"sm\"\n      disabled={disabled}\n      tabIndex={isTabStop ? 0 : -1}\n      {...itemProps}\n      className={cn(orientation === \"vertical\" && \"w-full\", className)}\n      ref={composedRef}\n      onClick={onClick}\n      onFocus={onFocus}\n      onKeyDown={onKeyDown}\n      onMouseDown={onMouseDown}\n    />\n  );\n}\n\ninterface ActionBarCloseProps extends React.ComponentProps<\"button\"> {\n  asChild?: boolean;\n}\n\nfunction ActionBarClose(props: ActionBarCloseProps) {\n  const { asChild, className, onClick, ...closeProps } = props;\n\n  const { onOpenChange } = useActionBarContext(CLOSE_NAME);\n\n  const onCloseClick = React.useCallback(\n    (event: React.MouseEvent<CloseElement>) => {\n      onClick?.(event);\n      if (event.defaultPrevented) return;\n\n      onOpenChange?.(false);\n    },\n    [onOpenChange, onClick],\n  );\n\n  const ClosePrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n  return (\n    <ClosePrimitive\n      type=\"button\"\n      data-slot=\"action-bar-close\"\n      {...closeProps}\n      className={cn(\n        \"rounded-xs opacity-70 outline-none hover:opacity-100 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className,\n      )}\n      onClick={onCloseClick}\n    />\n  );\n}\n\ninterface ActionBarSeparatorProps extends DivProps {\n  orientation?: Orientation;\n}\n\nfunction ActionBarSeparator(props: ActionBarSeparatorProps) {\n  const {\n    orientation: orientationProp,\n    asChild,\n    className,\n    ...separatorProps\n  } = props;\n\n  const context = useActionBarContext(SEPARATOR_NAME);\n  const orientation = orientationProp ?? context.orientation;\n\n  const SeparatorPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SeparatorPrimitive\n      role=\"separator\"\n      aria-orientation={orientation}\n      aria-hidden=\"true\"\n      data-slot=\"action-bar-separator\"\n      {...separatorProps}\n      className={cn(\n        \"in-data-[slot=action-bar-selection]:ml-0.5 in-data-[slot=action-bar-selection]:h-4 in-data-[slot=action-bar-selection]:w-px bg-border\",\n        orientation === \"horizontal\" ? \"h-6 w-px\" : \"h-px w-full\",\n        className,\n      )}\n    />\n  );\n}\n\nexport {\n  ActionBar,\n  ActionBarClose,\n  ActionBarGroup,\n  ActionBarItem,\n  type ActionBarProps,\n  ActionBarSelection,\n  ActionBarSeparator,\n};\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}